들어가며
<<웹 사이트 최적화 기법>>은 웹 사이트의 성능을 높이기 위한 열 네 가지 방법을 소개하고 있다. 그 중에 몇 가지 주제를 추려내고, 관련된 부분만 따로 정리하였다.
HTTP 요청을 줄여라
페이지 디자인 변경 없이 HTTP 요청을 줄이는 방법에 대해 이야기하고 있다. 주로 이미지 파일을 여러 번 요청하는 대신에, 한 번만 요청하여 사용할 수 있는 기법에 대해 이야기하고 있다.
CSS Sprite
사용하는 이미지를 하나의 파일로 묶어서 원격 서버에 배포한다. 그 다음에 background-position을 이용하여 필요한 부분을 잘라서 사용한다.
<div style="background-image: url('a_lot_of_sprites.gif');
background-position: -260px -90px
width: 26px; height: 24px;"
</div>이 경우, 분리된 이미지를 사용하는 것보다 57% 빠르다.

스크립트와 스타일시트의 결합
스크립트와 스타일시트를 분리하지 않고 하나로 합쳐서 클라이언트로 내려준다. 또한 모든 js를 하나로 만들어서 배포하는 것 또한 성능에 좋지 않다. 사용하지 않는 js까지 다운로드 받을 수 있기 때문이다.
- 개발 시에는 페이지마다 js를 따로 사용하고, 배포 시에 모듈을 합쳐서 서버로 배포한다.
- js를 하나로 만들어서 배포하기보다는, 각각 페이지에서 사용할 모듈 조합을 만드는 게 좋다.
- 하나로 만들어서 배포하면 필요하지 않은 파일까지 다운로드 받기 되기 때문
- 페이지가 여러 개인 웹 페이지에서는 주로 열두 개 가량의 모듈 조합이 필요함 (경험적)
최근에는
요청을 줄이는 게 더 중요해 진 거 같다.
- 글로벌 서비스에서는 레이턴시를 줄일 방법이 없기 때문
- 클라이언트가 뉴욕, 서버가 런던에 있는 경우 TCP 커넥션을 맺게 되면 최소 56ms가 소비됨
- 대부분의 시스템은 300ms 이상이 되면, 시스템을 느리다고 인식하게 됨

- 요청을 맺고 끊는데 드는 비용이 크기 때문(Three-hand shake)

서버 개선 방안
- 파이프라이닝
- 파이프라이닝은 구현하기가 어려움
- HOL 이슈 존재함
- 패킷의 라인이 첫 번째 패킷에 의해 블록될 때 발생함
- 도메인 샤딩
- 한 도메인당 최대 두 개 까지의 커넥션이 가능했지만, 현재는 일반적으로 최대 여섯 개까지의 병렬 커넥션 가능
- 현재는 성능 이슈 때문에 잘 사용하지 않음.
- Multiplexing(HTTP 2.0)
- TCP 연결이 되면, 모든 요청은 TCP 커넥션을 통해 수행됨.
- 여러 요청은 프레임으로 분할되어, 각각의 스트림 ID가 부여됨
- 여러 스트림의 모든 프레임이 비동기적으로 리퀘스트 / 서버는 비동기적으로 응답
- 클라이언트는 스트림 ID에 따라 프레임을 정렬함

CSS Sprite
- 이미지를 하나로 합쳐서 배포하는 기술은 아직도 여러 곳에서 사용중
- grunt-spritesmith 를 이용하여, 자동으로 이미지 스프라이트가 되게 할 수 있다.
- 특정 폴더에 이미지를 넣어 두고, grunt task를 실행하면 자동으로 이미지를 sprite 할 수 있다.
- link
스크립트 압축
- 의존성 있는 스크립트를 하나로 합칠 수 있게 하는 다양한 라이브러리 존재
- Webpack
- CommonJS와 AMD 두 가지 의존성 관리 포멧을 모두 지원함
- 의존성 있는 모듈끼리 합쳐서 하나의 js 파일로 만들 수 있음
- webpack
- 이외에도 다양한 기능 제공
- 로더
- 모듈 의존성 관리
헤더에 만료기간을 추가하라
헤더에 만료기한을 추가하여 구성요소를 캐시에 저장할 수 있다. 헤더 만료기산은 스크립트, 스타일시트, 플래시와 같은 모든 구성요소에서 사용하는 것이 좋다.
만료기한
- Expires 를 주어서, HTTP응답이 더 이상 유효하지 않은 시간을 선언할 수 있다.
- 이 값은 헤더 안에 포함하여 보낸다.
Expires: Thu, 15 Apr 2010 20:00:00 GMTmax-age와 mod_expires 속성
- Expires의 문제점
- Expires 속성은 지정된 날짜를 이용하기 때문에, 서버와 클라이언트 간에 시간을 맞춰서 이용해야 하는 문제점이 있음
- 만료 날짜를 확인해야 하고, 만료되면 갱신시켜줘야 함
- Cache-Control은 max-age 속성을 이용해, 구성요소를 캐시에 보관할지 설정
Cache-Control: max-age=315360000- HTTP 1.0 사용자를 위해, max-age와 Expires를 둘 다 설정해주면 좋음
- 아파치의 mod_expires 모듈을 이용하면 된다.
파일 이름의 활용
- 캐시에 저장된 리소스를 비우고, 새로운 리소스를 다운받게 하고 싶을 때
- 파일 이름을 변환하여 리소스를 새로 받게 한다.
Expires를 주지 않은 경우?
- 조건부 Get 요청을 날려, 캐시의 유효기간을 확인한다.
- 304 Not Modified + 응답 데이터가 없는 응답이 오면 캐시에서 데이터 꺼내서 사용
- 헤더에 미리 기간을 담아서 내려주면, 조건부 GET 요청을 날리지 않으므로 그 시간이 절약됨
Cache-Control의 기타 옵션
- public vs private
- no-store vs no-cache
전략

- HTML은 no-cache로 표현한다.
- CSS는 브라우저와 중간 캐시(CDN)에 의해 캐시될 수 있다. 업데이트시 URL을 변경한다.
- 자바스크립트는 1년 만에 만료되도록 설정한다. 개인 사용자 데이터가 있으므로 private로 설정한다.
- 자주 업데이트 되는 코드는 별도의 파일로 제공한다. 이렇게 하면 자주 변경되지 않는 부분은 캐시에서 가져올 수 있게 된다.
사례 확인
- 네이버 : HTML은 no-cache로 표현. 스크립트, 이미지, CSS모두 캐싱됨. 파일 이름을 변경하여 리소스를 리로드함. 이유는 잘 모르겠지만 캐시가 되지 않는 정적 리소스가 조금 있음
- 아마존 : HTML을 제외한 모든 리소스가 캐싱됨. 캐싱 기간이 대략 10년 근방. 로딩하는 리소스가 많은데, 체감 속도는 빠름.
- 구글 : 로딩하는 데이터의 양이 가장 적음. 모든 데이터가 캐싱됨. 캐싱 기간이 페이스북과 더불어 가장 짧음(대략 1년)
- 페이스북 : 모든 정적 리소스가 캐싱됨. 캐싱 기간이 대략 1년 정도. 정말 짧은 정적 리소스는 2주 캐싱되는 경우도 있음.
Gzip 컴포넌트
페이지를 전송하기 전에 압축해서 보낼 수 있다.
적용방법
- 클라이언트가 요청할 시 헤더에 해당 정보를 담는다.
Accept-Encoding:gzip, deflate- 서버는 위의 요청을 보고, 클라이언트가 지정한 압축 방식 중 하나로 응답을 압축한다. Content-Encoding을 이용하여 클라이언트에게 알려준다.
Content-Encoding: gzip무엇을 압축해야 하는가?
- HTML, 스크립트, CSS는 압축하면 좋다.
- PDF와 이미지는 이미 압축된 파일이기 때문에, 압축을 적용해서는 안 된다.
- 압축을 하고 / 푸는 데 추가적인 CPU 비용이 들어간다. 대략적으로 파일이 1~2 KB보다 크면 압축을 적용해 볼 만 하다.
사례
- 아마존 : gzip으로 압축. HTML, JS, CSS를 모두 압축한다.
- 사이즈가 작은(500~600 Byte) 것도 압축함
- 페이스북 : br로 압축
- Brotli, 오픈 소스 데이터 압축 라이브러리
- 페이스북도 사이즈가 작은 것도 압축함
- 네이버 : gzip으로 압축 / css, js 모두 압축
- CNN : gzip으로 압축 / css, js 모두 압축
- 구글 : gzip으로 압축 / css, js 모두 압축
스타일시트는 위에 넣어라
페이지의 헤더, 내비게이션 바, 상위의 로고 등의 다운로드 되는 대로 바로 그릴 수 있어야 한다. 스타일시트를 페이지 하단에 넣으면, 브라우저는 스타일이 변경되었을 경우 다시 그려야 하는 것을 피하기 위해 랜더링을 하지 않고 기다린다. 따라서 스타일시트는 상단에, 태그 안에 태그를 이용하여 선언하는 것이 좋다.
이유
- 스타일 시트가 로딩 중인데도 랜더링 트리를 구성하는 것은 비효율적. 스타일 시트가 로드 완료되고, 분석되면 화면을 다시 그려야 할 수 있기 떄문
- 화면을 점진적으로 그리고, 스타일 시트가 로딩된 후 화면을 다시 그리는 것을 FOUS 현상이라고 하고, 이는 반드시 피해야 한다.
- 따라서 FOUS 현상을 피하기 위해, 스타일 시트를 받을때까지는 화면의 구성요소를 점진적으로 그리지 않는다.
규칙
- 75k 보다 작은 한 개의 외부 css 파일을 구성한다.
- CSS 파일이 여러 개라면 한 개로 합치면 좋다. 파일이 여러 개면 여러 번 리퀘스트 해야 하기 때문
- CSS 가 작은 경우, 페이지의 상단에 테그를 이용하여 CSS 를 HTML안에 둔다
- @import url(“style.css”)와 같은 건 사용하지 않는다. (link보다 느리다. 화면 하단에 스타일시트를 두는 것과 같음)
-
를 사용하지 않는다.
- 코드 중복이 발생한다.
- Content Security Policy(link)를 위반한다.
- 문서에 사용자가 스타일을 수정할 수 있는 경우엔, 교차 스타일 공격을 받을 수 있음(사이트의 로그인 버튼을 변경한다던지…)
Mitigate the risk of content-injection attacks by giving developers fairly granular control over
1. The resources which can be requested (and subsequently embedded or executed) on behalf of a specific Document or Worker
2. The execution of inline script
3. Dynamic code execution (via eval() and similar constructs)
4. The application of inline style스크립트는 아래에 넣어라
스크립트는 다운로드 시간 동안에는 브라우저가 랜더링이 되지 않는다. 따라서 화면이 하얗게 나오는 현상이 발생할 수 있다. 또한 스크립트 다운로드 시간 중에는, 동시 다운로드가 막히게 되어 전체적인 성능 저하에 영향을 미치게 된다.
스크립트를 위에 넣는 경우
- 스크립트를 받는 동안 동시 다운로드가 되지 않음
- 스크립트를 받는 동안 화면에 렌더링이 막히므로, 빈 흰색 스크린 현상이 발생할 수 있다.
스크립트를 아래에 넣는 경우
- 눈에 보이는 구성요소는 일찍 다운로드된다. 화면이 뭔가 그려지는 느낌이 들어서 사용자에게 좋다.
- 동시 다운로드가 막히지 않고 진행된다.
최근 브라우저에서는
- 스크립트 여러 개를 동시에 받거나, 스크립트를 받는 동안 gif는 다운받을 수 있다.
- 다운받는 시간 동안 화면을 그릴 수 없는 것은 같다.


ETag를 설정하라
흐름
- 브라우저가 구성요소를 다운로드하면 캐시에 저장한다. 이후에 페이지 방문시, 캐시된 구성요소가 새 버전이라면 브라우저는 HTTP 요청을 하지 않고 디스크에서 읽는다.
- 만약에 구성요소가 만료되었거나, 리프레시를 하면 조건부 GET 요청을 하게 된다. 캐시가 유효한 경우, 서버는 304 Not Modified 상태 코드를 리턴한다.
위의 경우에서 서버가 구성요소가 같은 파일인지 결정하는 두가지 방법이 있다.
- 마지막 수정일 비교
- ETag 비교
마지막 수정일
구성요소의 마지막 수정일은, 서버에서 Last-Modified를 통하여 반환된다.
Accept-Ranges:bytes
Access-Control-Allow-Origin:*
Age:555321
Cache-Control:max-age=604800
Content-Length:32611
Content-Type:image/png
Date:Wed, 14 Dec 2016 01:14:47 GMT
Expires:Wed, 21 Dec 2016 01:14:47 GMT
Last-Modified:Tue, 30 Aug 2016 09:05:24 GMT
Server:Testa/4.8.6반환된 Last-Modified 값을 브라우저에서는 캐시에 저장한다. 이후에 같은 데이터가 요청되면, 브라우저는 If-Modified-Since 헤더를 이용하여 마지막 수정일을 서버로 전달한다. Last-Modified == IF=Modified-Since 면, 304 코드를 반환하고 데이터는 주지 않는다.
ETag
- ETag는
- 서버는 구성요소의 ETag를 응답 해더의 Etag에 저장하여 클라이언트에 내려준다.
- 조건부 GET 요청시에 서버로 값을 전달하여(If-None-Match 헤더에 담아준다), 정확한 값인지 확인
- User-Agent나 Accept-Language 값에 따라 요청을 변경해야 하는 경우, 이 값을 변경하면 된다.
Accept-Ranges:bytes
Age:2207
Cache-Control:max-age=31536000
Content-Length:63
Content-Type:image/gif
Date:Tue, 20 Dec 2016 11:02:14 GMT
ETag:"3f-43cd4afeb2840"
Expires:Wed, 20 Dec 2017 11:02:14 GMT
Last-Modified:Fri, 19 Oct 2007 08:54:49 GMT
Server:Testa/4.8.6
문제점
- 호스트하는 서버에서만 값이 유효하다.
- 한 서버에 GET 요청을 하여 값을 가져오고, 조건부 GET을 다른 서버로 보냈을 때 값이 달라질 수 있다.
- 서버마다 다르지만, Etag를 구성할 떄 : INode (the most server-specific) + File size + Last modified time 이런 값들을 사용하여 구성한다.
- 서버마다 INode가 다르기 때문에 Etag가 다를 수 있다.
- 프록시 캐시의 효율 또한 떨어뜨린다.
- 프록시를 사용하는 사용자의 브라우저에 캐시된 ETag값이 프록시의 ETag값과 일치하지 않을 수 있다.
- If-Modified-Since와 If-None-Match를 둘 다 사용하는 경우, ETag값이 다르면 304 응답을 내려줄 수 없다. 이 경우에는 Etag를 안쓰는게 낫다.
해결책
- Inode 값을 제거한다.
- 이 경우 Last-Modified 헤더와 같은 정보가 내려가게 됨
- ETag를 삭제한다.
- 네이버는 값이 잘 내려오고 있는데 어떻게 하는지는 잘 모르겠음